package br.edu.ufcg.splab.io;

import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

import br.edu.ufcg.splab.core.InterfaceVertex;
import br.edu.ufcg.splab.core.edges.EdgeType;
import br.edu.ufcg.splab.core.graph.Graph;
import br.edu.ufcg.splab.exceptions.LTSBTException;
import br.edu.ufcg.splab.exceptions.ParseException;


public class TGFParser {
	
	private enum TokenType{
		Number, Word, Hash
	}
	
	private class Token{
		String text;
		TokenType type;
		public Token(String text, TokenType type){
			this.text = text;
			this.type = type;
		}
	}
	
	public Graph parseTGF(String fileName) throws ParseException{
		List<Token> tokens = this.getTokens(fileName);
		int hashIndex = hashIndex(tokens);
		if(hashIndex == -1){ //there is no hash symbol in the tgf file
			throw new ParseException("The TGF file must have a \"#\" separating the nodes and edges definition.");
		}else if(hashIndex == 0){ //there is no node defined in the tgf file
			throw new ParseException("The TGF file must define at least one node.");
		}
		int i = 0;
		int nodeNumber = 1;
		Graph returnGraph = new Graph();
		while(i < hashIndex){
			Token first = tokens.get(i);
			Token second = tokens.get(i + 1);
			if(first.type != TokenType.Number || second.type != TokenType.Number){
				throw new ParseException("In the definition of the node " + nodeNumber + ", the values must be two integer numbers.");
			}
			if(!first.text.equals(second.text)){
				throw new ParseException("In the definition of the node " + nodeNumber + ", the values must be equal.");
			}
			returnGraph.addVertex(second.text);
			i += 2;
			nodeNumber++;
		}
		i = hashIndex + 1;
		int edgeNumber = 1;
		while(i < tokens.size()){
			Token first = tokens.get(i);
			i++;
			Token second = tokens.get(i);
			i++;
			if(first.type != TokenType.Number || second.type != TokenType.Number){
				throw new ParseException("In the definition of the edge " + edgeNumber + ", the from and two values must be two integer numbers.");
			}
			if(this.getVertex(returnGraph, first.text) == null){
				throw new ParseException("In the definition of the edge " + edgeNumber + ", the node " + first.text + " must be defined previously.");
			}
			if(this.getVertex(returnGraph, second.text) == null){
				throw new ParseException("In the definition of the edge " + edgeNumber + ", the node " + second.text + " must be defined previously.");
			}
			boolean labelCompleted = false;
			boolean endOfTokens = false;
			String label = "";
			Token third = null;
			do{
				if(i >= tokens.size()){
					endOfTokens = true;
					continue;
				}
				third = tokens.get(i);
				if(third.type.equals(TokenType.Word)){
					label += (third.text + " ");
					i++;
				}else if(label.equals("")){
					throw new ParseException("In the definition of the edge " + edgeNumber + ", no label was defined.");
				}else{
					labelCompleted = true;
				}
			}while(!labelCompleted && !endOfTokens);
			try {
				if(label.equals("")){
					throw new ParseException("In the definition of the edge " + edgeNumber + ", no label was defined.");
				}else{
					returnGraph.createEdge(this.getVertex(returnGraph, first.text).getUUID(), this.getVertex(returnGraph, second.text).getUUID(), label, EdgeType.DEFAULT);
				}
			} catch (LTSBTException e) {
			}
			edgeNumber++;
		}
		this.semanticVerification(returnGraph);
		return returnGraph;
		
	}
	
	private void semanticVerification(Graph graph) throws ParseException {
		//The graph must have just one root, in another words, just one node must have no parent node;
		List<String> roots = new ArrayList<String>();
		for (InterfaceVertex vertex : graph.getStates().values()) {
			if(vertex.getInTransitions().size() == 0 && vertex.getOutTransitions().size() > 0){
				roots.add(vertex.getLabel());
			}
		}
		if(roots.size() > 1){
			String message = "The graph contains more than one root node: ";
			for (int i = 0; i < roots.size(); i++) {
				String label = roots.get(i);
				message += label;
				if(i != roots.size() - 1){
					message += ", ";
				}
			}
			message += ". Just the first node defined in the file must be the root.";
			throw new ParseException(message);
		}
		
		//The graph must not have any disconnected node;
		List<String> disconnected = new ArrayList<String>();
		for (InterfaceVertex vertex : graph.getStates().values()) {
			if(vertex.getInTransitions().size() == 0 && vertex.getOutTransitions().size() == 0){
				disconnected.add(vertex.getLabel());
			}
		}
		if(disconnected.size() > 0){
			String message = "The graph contains at least one disconnected node: ";
			for (int i = 0; i < disconnected.size(); i++) {
				String label = disconnected.get(i);
				message += label;
				if(i != disconnected.size() - 1){
					message += ", ";
				}
			}
			message += ". The graph cannot contain disconnected nodes.";
			throw new ParseException(message);
		}
	}
	
	private InterfaceVertex getVertex(Graph graph, String label){
		for (InterfaceVertex vertex: graph.getStates().values()) {
			if(vertex.getLabel().equals(label)){
				return vertex;
			}
		}
		return null;
	}

	private List<Token> getTokens(String fileName) {
		try {
			List<Token> tokenList = new ArrayList<Token>();
			Scanner scanner = new Scanner(new File(fileName));
			while(scanner.hasNext()){
				String nextToken = scanner.next();
				tokenList.add(new Token(nextToken, this.tokenType(nextToken)));
			}
			return tokenList;
		} catch (FileNotFoundException e) {
			return null;
		}
	}

	private TokenType tokenType(String nextToken) {
		try{
			Integer.parseInt(nextToken);
			return TokenType.Number;
		}catch(NumberFormatException e){
			if(nextToken.equals("#")){
				return TokenType.Hash;
			}else{
				return TokenType.Word;
			}
		}
	}

	private int hashIndex(List<Token> tokens) {
		for (int i = 0; i < tokens.size(); i++) {
			if(tokens.get(i).type.equals(TokenType.Hash)){
				return i;
			}
		}
		return -1;	
	}
}
